<?php

namespace W3;

use SplFileObject;
use ErrorException;

/**
 * 文件处理类
 *
 * @author edikud
 * @date 2022/10/22
 * @copyright Copyright (c) 2022 W3 (http://www.mcooo.com)
 * @license GNU General Public License 2.0
 */
class Filer
{
    /**
     * Mime类型列表
     *
     * @var array
     */
    public static $mime_types = [
        'aac'        => 'audio/aac',
        'atom'       => 'application/atom+xml',
        'avi'        => 'video/avi',
        'bmp'        => 'image/x-ms-bmp',
        'c'          => 'text/x-c',
        'class'      => 'application/octet-stream',
        'css'        => 'text/css',
        'csv'        => 'text/csv',
        'deb'        => 'application/x-deb',
        'dll'        => 'application/x-msdownload',
        'dmg'        => 'application/x-apple-diskimage',
        'doc'        => 'application/msword',
        'docx'       => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'exe'        => 'application/octet-stream',
        'flv'        => 'video/x-flv',
        'gif'        => 'image/gif',
        'gz'         => 'application/x-gzip',
        'h'          => 'text/x-c',
        'htm'        => 'text/html',
        'html'       => 'text/html',
        'ini'        => 'text/plain',
        'jar'        => 'application/java-archive',
        'java'       => 'text/x-java',
        'jpeg'       => 'image/jpeg',
        'jpg'        => 'image/jpeg',
        'js'         => 'text/javascript',
        'json'       => 'application/json',
        'mid'        => 'audio/midi',
        'midi'       => 'audio/midi',
        'mka'        => 'audio/x-matroska',
        'mkv'        => 'video/x-matroska',
        'mp3'        => 'audio/mpeg',
        'mp4'        => 'application/mp4',
        'mpeg'       => 'video/mpeg',
        'mpg'        => 'video/mpeg',
        'odt'        => 'application/vnd.oasis.opendocument.text',
        'ogg'        => 'audio/ogg',
        'pdf'        => 'application/pdf',
        'php'        => 'text/x-php',
        'png'        => 'image/png',
        'psd'        => 'image/vnd.adobe.photoshop',
        'py'         => 'application/x-python',
        'ra'         => 'audio/vnd.rn-realaudio',
        'ram'        => 'audio/vnd.rn-realaudio',
        'rar'        => 'application/x-rar-compressed',
        'rss'        => 'application/rss+xml',
        'safariextz' => 'application/x-safari-extension',
        'sh'         => 'text/x-shellscript',
        'shtml'      => 'text/html',
        'swf'        => 'application/x-shockwave-flash',
        'tar'        => 'application/x-tar',
        'tif'        => 'image/tiff',
        'tiff'       => 'image/tiff',
        'torrent'    => 'application/x-bittorrent',
        'txt'        => 'text/plain',
        'wav'        => 'audio/wav',
        'webp'       => 'image/webp',
        'wma'        => 'audio/x-ms-wma',
        'xls'        => 'application/vnd.ms-excel',
        'xml'        => 'text/xml',
        'zip'        => 'application/zip',
    ];
	
    /**
     * 确定文件或目录是否存在
     *
     * @param  string  $path
     * @return bool
     */
    public static function exists(string $path): bool
    {
        return file_exists($path);
    }

    /**
     * 确定给定路径是否为文件
     *
     * @param  string  $file
     * @return bool
     */
    public static function isFile(string $file)
    {
        return is_file($file);
    }

    /**
     * 确定给定路径是否可写
     *
     * @param  string  $path
     * @return bool
     */
    public static function isWritable(string $path)
    {
        return is_writable($path);
    }

    /**
     * 确定给定路径是否可读
     *
     * @param  string  $path
     * @return bool
     */
    public static function isReadable(string $path)
    {
        return is_readable($path);
    }

    /**
     * 获取文件的内容 (反复获取文件, 超时设置为1ms)
     *
     * @param  string  $path
     * @param String $mode  操作方式，默认为读操作，可供选择的项为：r,r+,w+,w+,a,a+
     * @return mixed
     * @return string
     */
    public static function get(string $file, string $mode = 'rb')
    {
        $result = NULL;
        if ($fp = fopen($file, $mode)) {
            $startTime = microtime();
            do {
                // 用共享锁模式打开文件并读取文章，可以避免在并发写入造成的读取不完整问题
                $can_read = flock($fp, LOCK_SH);
                if (!$can_read) {
                    usleep(round(rand(0, 100) * 1000));
                }
            } while (!$can_read && microtime() - $startTime < 1000);
            if ($can_read) {
                $file_size = filesize($file);
                $file_size && ($result = fread($fp, $file_size));
                //while(!feof($fp)) {
                //    $result.= fread($fp, 1024);
                //}
                # 返回false表示已经读取到文件末尾
                //while(false != ($a = fread($fp, 1024))){
                //    $result .= $a;
                //}
                flock($fp, LOCK_UN);
            }
            fclose($fp);
        }
        return $result;
    }

    /**
     * 写入文件数据  (反复写入文件, 超时设置为1ms)
     *
     * @param string $file 路径地址
     * @param string $contents 要写入的数据
     * @param String $mode  操作方式，默认为读操作，可供选择的项为：r,r+,w+,w+,a,a+
     * @return bool
     */
    public static function put(string $file, string $contents, string $mode = 'wb')
    {
        $result = false;
        if ($fp = fopen($file, $mode)) {
            $startTime = microtime();
            do {
                $can_write = flock($fp, LOCK_EX);
                if (!$can_write) {
                    usleep(round(rand(0, 100) * 1000));
                }
            } while (!$can_write && microtime() - $startTime < 1000);
			
            if ($can_write) {
                false !== fwrite($fp, $contents) && ($result = true);
                flock($fp, LOCK_UN);
            }
            fclose($fp);
        }
        $result && static::extension($file) == 'php' && Opcache::invalid($file);
        return $result;
    }

    /**
     * 获取具有共享访问权限的文件内容
     *
     * @param  string  $path
     * @return string
     */
    public static function shared(string $path)
    {
        $contents = '';

        $handle = fopen($path, 'rb');

        if ($handle) {
            try {
                if (flock($handle, LOCK_SH)) {
                    clearstatcache(true, $path);

                    $contents = fread($handle, static::size($path) ?: 1);

                    flock($handle, LOCK_UN);
                }
            } finally {
                fclose($handle);
            }
        }

        return $contents;
    }

    /**
     * @brief  递归创建文件夹
     * @param String $dir  路径
     * @param int    $chmod 文件夹权限
     * @note  $chmod 参数不能是字符串(加引号)，否则linux会出现权限问题
     */
    public static function mkdir(string $dir, int $chmod = 0777)
    {
        //return is_dir($dir) || mkdir($dir, $chmod, true);
        return is_dir($dir) || self::mkdir(dirname($dir), $chmod) && mkdir($dir, $chmod);
    }

    /**
     * 写入文件的内容，如果文件已经存在，则替换它
     *
     * @param  string  $file
     * @param  string  $content
     * @return void
     */
    public static function replace(string $file, string $content)
    {
        return static::put($file, $content, 'w+');
    }

    /**
     * Prepend to a file.
     *
     * @param  string  $path
     * @param  string  $content
     * @return int
     */
    public static function prepend(string $path, string $content)
    {
        return static::put($path, $content, 'a+');
    }

    /**
     * 附加到文件
     *
     * @param  string  $path
     * @param  string  $content
     * @return int
     */
    public static function append(string $path, string $content)
    {
        return static::put($path, $content, 'r+');
    }

    /**
     * 删除给定路径上的文件
     *
     * @param  string|array  $file
     * @return bool
     */
    public static function delete(string $file)
    {
        $success = true;

        try {
            if (! @unlink($file)) {
                $success = false;
            }
        } catch (ErrorException $e) {
            $success = false;
        }
		
		$success && static::extension($file) == 'php' && Opcache::invalid($file);

        return $success;
    }

    /**
     * Extract the file name from a file path.
     *
     * @param  string  $path
     * @return string
     */
    public static function name(string $path)
    {
        return pathinfo($path, PATHINFO_FILENAME);
    }

    /**
     * Extract the trailing name component from a file path.
     *
     * @param  string  $path
     * @return string
     */
    public static function basename(string $path)
    {
        return pathinfo($path, PATHINFO_BASENAME);
    }

    /**
     * Extract the parent directory from a file path.
     *
     * @param  string  $path
     * @return string
     */
    public static function dirname(string $path)
    {
        return pathinfo($path, PATHINFO_DIRNAME);
    }

    /**
     * Extract the file extension from a file path.
     *
     * @param  string  $path
     * @return string
     */
    public static function extension(string $path)
    {
        return pathinfo($path, PATHINFO_EXTENSION);
    }

    /**
     * Get the file type of a given file.
     *
     * @param  string  $path
     * @return string
     */
    public static function type(string $path)
    {
        return filetype($path);
    }

    /**
     * 返回文件的mime类型。如果未找到mime类型，则返回false。
     *
     *  <code>
     *      echo File::mime('filename.txt');
     *  </code>
     *
     * @param  string  $file  Full path to the file
     * @param  boolean $guess Set to false to disable mime type guessing
     * @return string
     */
    public static function mime(string $file, $default = false)
    {
        // Redefine vars
        $file  = (string) $file;
        $mime = false;

        // Get mime using the file information functions
        if (function_exists('finfo_open')) {

            $info = finfo_open(FILEINFO_MIME_TYPE);

            $mime = finfo_file($info, $file);

            finfo_close($info);

        } else {

            $mime_types = static::$mime_types;

            $extension = pathinfo($file, PATHINFO_EXTENSION);

            isset($mime_types[$extension]) && $mime = $mime_types[$extension];
        }
		
		return $mime ?: $default;
    }

    /**
     * 获取文件大小
     *
     * @param  string  $path
     * @return int
     */
    public static function size(string $path): int
    {
        return filesize($path);
    }

    /**
     * 获取文件的上次修改时间
     *
     * @param  string  $path
     * @return int
     */
    public static function lastModified(string $path)
    {
        return filemtime($path);
    }

    /**
     * 确定给定路径是否为目录
     *
     * @param  string  $directory
     * @return bool
     */
    public static function isDir(string $directory): bool
    {
        return is_dir($directory);
    }

    /**
     * Find path names matching a given pattern.
     *
     * @param  string  $pattern
     * @param  int  $flags
     * @return array
     */
    public static function glob(string $pattern, int $flags = 0)
    {
        return glob($pattern, $flags);
    }

    /**
     * Get or set UNIX mode of a file or directory.
     *
     * @param  string  $path
     * @param  int|null  $mode
     * @return mixed
     */
    public static function chmod(string $path, ?int $mode = null)
    {
        if ($mode) {
            return chmod($path, $mode);
        }

        return substr(sprintf('%o', fileperms($path)), -4);
    }

    /*
    // 获取大文件的总行数
    function lines($file)
    {
      $l = 0;
      $fp = fopen($file, 'r');
      while(fgets($fp)) $l++;
      fclose($fp);
      return $l;
    }
    */
	
    /**
     * 获取大文件的总行数
     * 
     * @return int 返回行数
     */
    public static function lines(string $file): int
    {
        $fp = new SplFileObject($file, 'r');
        // 这种方式效率略低
        // $fp->seek(filesize($this->file));
        // return $fp->key();
        $sum = 0;
        while ($fp->valid()) {
            $data = $fp->fread(1024 * 1024 * 2);
            //每次读取2M
            $num = substr_count($data, PHP_EOL);
            //计算换行符出现的次数
            $sum += $num;
        }
        unset($fp);
        return $sum;
    }
	
    /**
     * 读取指定行区间的数据
     * 
     * @param $start 开始的行号
     * @param $num 读取的行数
     * @return $buf 返回读取到的行数组
     */
    public static function slice(string $file, int $start, int $num)
    {
        if ($start <= 0 || $num <= 0 || !is_int($start) || !is_int($num)) {
            throw new Exception("参数不正确，请输入大于0的整数");
        }
        $fp = new SplFileObject($file, 'r');
        $buf = [];
        // SplFileObject的seek方法索引从0开始
        $fp->seek($start - 1);
        while ($num > 0 && $fp->valid()) {
            $buf[] = $fp->current();
            $fp->next();
            $num--;
        }
        unset($fp);
        return $buf;
    }
	
    /**
     * 读取末尾N条数据
     * 
     * @param $num 读取的行数
     * @return $buf 返回读取到的行数组
     */
    public static function tail(string $file, int $num)
    {
        if ($num <= 0 || !is_int($num)) {
            throw new Exception("参数不正确，请输入大于0的整数");
        }
        $fp = fopen($file, "r");
        $pos = -2;
        $eof = '';
        //当总行数小于Num时，判断是否到第一行了
        $head = false;
        $buf = [];
        while ($num > 0) {
            while ($eof != PHP_EOL) {
                // fseek成功返回0，失败返回-1
                if (fseek($fp, $pos, SEEK_END) == 0) {
                    $eof = fgetc($fp);
                    $pos--;
                } else {
                    //当到达第一行，行首时，设置$pos失败
                    fseek($fp, 0, SEEK_SET);
                    //到达文件头部，开关打开
                    $head = true;
                    break;
                }
            }
            array_unshift($buf, fgets($fp));
            // 这一句，只能放上一句后，因为到文件头后，把第一行读取出来再跳出整个循环
            if ($head) {
                break;
            }
            $eof = '';
            $num--;
        }
        fclose($fp);
        return $buf;
    }
}